﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TibcoQueueMonitor.Entities;
using System.IO;
using System.Text.RegularExpressions;

namespace TibcoQueueMonitor.TibcoAdminManagers
{
    /// <summary>
    /// Manager object that parses data that comes from the tibcoemsadmin.exe
    /// utility's output.
    /// </summary>
    public static class TibcoAdminDataParser
    {
        #region Public Methods
        /// <summary>
        /// Parses the console output stream generated by the tibcoemsadmin.exe
        /// utility.
        /// </summary>
        /// <param name="sr">The stream reader that encapsulates the console
        /// output.</param>
        /// <returns></returns>
        public static TibcoAdministrationData ParseStream(StreamReader sr)
        {
            StringBuilder sb = new StringBuilder();
            ParseMode currentParseMode = ParseMode.None;
            // Initialize an index of offset objects. The key for the outer
            // index is the parse type, while the key for the inner index is
            // the offset object's "ColumnName"
            Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetLookupIndex = new Dictionary<ParseMode, Dictionary<string, ColumnOffset>>();
            TibcoAdministrationData data = new TibcoAdministrationData();

            while (sr.EndOfStream == false)
            {
                char buf = (char)sr.Read();
                switch (buf)
                {
                    case Constants.Newline:
                    case Constants.CarriageReturn:
                        if (sr.Peek() == (int)Constants.Newline)
                            // absorb this carriage return and next
                            // linefeed character then proceed.
                            sr.Read();
                        // Parse line
                        ParseLine(ref currentParseMode, ref offsetLookupIndex, sb, ref data);

                        // Wipe buffer and continue reading
                        sb.Remove(0, sb.Length);
                        break;
                    default:
                        sb.Append(buf);
                        break;
                }
            }

            return data;
        }

        #endregion //Public Methods

        #region Private Methods
        private static void ParseLine(ref ParseMode currentParseMode, ref Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetIndex, StringBuilder sb, ref TibcoAdministrationData data)
        {
            if (sb.Length > 0)
            {
                switch (sb.ToString())
                {
                    case Constants.CommandQueues:
                        currentParseMode = ParseMode.Queues;
                        offsetIndex.Add(currentParseMode, new Dictionary<string, ColumnOffset>());
                        break;
                    case Constants.CommandConnections:
                        currentParseMode = ParseMode.Connections;
                        offsetIndex.Add(currentParseMode, new Dictionary<string, ColumnOffset>());
                        break;
                    case Constants.CommandConsumers:
                        currentParseMode = ParseMode.Consumers;
                        offsetIndex.Add(currentParseMode, new Dictionary<string, ColumnOffset>());
                        break;
                    default:
                        // Process the string under the current mode
                        switch (currentParseMode)
                        {
                            case ParseMode.None:
                                if (Regex.IsMatch(sb.ToString(), @"error=\d+"))
                                {
                                    // Capture error number
                                    string errorNumToken = Regex.Match(sb.ToString(), @"error=\d+").Value;
                                    int errorNum = int.Parse(Regex.Match(errorNumToken, @"\d+").Value);

                                    throw new TibcoAdminConnectionException(sb.ToString(), errorNum);
                                }
                                break;
                            case ParseMode.Queues:
                                TibcoQueue parsedQueue = ParseQueueRow(sb, ref offsetIndex);
                                if (parsedQueue != null)
                                    data.Queues.Add(parsedQueue.QueueName, parsedQueue);
                                break;
                            case ParseMode.Connections:
                                TibcoConnection parsedConnection = ParseConnectionRow(sb, ref offsetIndex);
                                if (parsedConnection != null)
                                    data.Connections.Add(parsedConnection.ConnectionID, parsedConnection);
                                break;
                            case ParseMode.Consumers:
                                TibcoConsumer parsedConsumer = ParseConsumerRow(sb, ref offsetIndex);
                                if (parsedConsumer != null)
                                    data.Consumers.Add(parsedConsumer.ConnectionID, parsedConsumer);
                                break;
                            default:
                                break;
                        }
                        break;
                }
            }
        }

        private static TibcoQueue ParseQueueRow(StringBuilder sb, ref Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetIndex)
        {
            Dictionary<string, ColumnOffset> queueOffsetIndex = offsetIndex[ParseMode.Queues];
            TibcoQueue queue = new TibcoQueue();

            // If no offsets have been configured, we assume the current row
            // contains the header labels. The size of these columns are
            // obtained here.
            if (queueOffsetIndex.Count == 0)
            {
                // Get size of Queue Name column
                GenerateOffset(ref queueOffsetIndex, sb, Constants.OffsetHeaderQueuesQueueName, false, ColumnType.First, 2);

                // Get size of flags column
                GenerateOffset(ref queueOffsetIndex, sb, Constants.OffsetHeaderQueuesSNFG, false, ColumnType.Middle);

                // Get size of receivers column
                GenerateOffset(ref queueOffsetIndex, sb, Constants.OffsetHeaderQueuesReceivers, true, ColumnType.Middle, 1);
                
                // Get size of message count column
                GenerateOffset(ref queueOffsetIndex, sb, Constants.OffsetHeaderQueuesMessages, true, ColumnType.Middle);

                return null;
            }

            queue.QueueName = GetColumnValue(queueOffsetIndex, sb, Constants.OffsetHeaderQueuesQueueName);

            queue.Receivers = int.Parse(GetColumnValue(queueOffsetIndex, sb, Constants.OffsetHeaderQueuesReceivers));

            queue.MessageCount = int.Parse(GetColumnValue(queueOffsetIndex, sb, Constants.OffsetHeaderQueuesMessages));

            // Calculating the 'Size' column requires custom behavior apart
            // from other columns.
            int sizeColumnOffset = queueOffsetIndex[Constants.OffsetHeaderQueuesMessages].Position +
                queueOffsetIndex[Constants.OffsetHeaderQueuesMessages].ColumnLength + 1;

            string messageSizeToken = sb.ToString().Substring(sizeColumnOffset).Trim();
            float messageSize = float.Parse(messageSizeToken.Substring(0, messageSizeToken.Length - 2).Trim());
            switch (messageSizeToken.Substring(messageSizeToken.Length - 2).Trim())
            {
                case Constants.Kilobyte:
                    queue.TotalMessageSizeKB = messageSize;
                    break;
                case Constants.Megabyte:
                    queue.TotalMessageSizeKB = messageSize * 1024;
                    break;
            }

            return queue;
        }

        private static TibcoConnection ParseConnectionRow(StringBuilder sb, ref Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetIndex)
        {
            Dictionary<string, ColumnOffset> connectionOffsetIndex = offsetIndex[ParseMode.Connections];
            TibcoConnection conn = new TibcoConnection();

            // If no offsets have been configured, we assume the current row
            // contains the header labels. The columns offsets are
            // obtained here.
            if (connectionOffsetIndex.Count == 0)
            {
                //// Determine offset of 'L' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsClientType, false, ColumnType.First);

                //// Determine offset of 'ID' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsId, false, ColumnType.Middle);

                //// Determine offset of 'FSXT' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsFSXT, false, ColumnType.Middle);

                //// Determine offset of 'Host' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsHost, false, ColumnType.Middle);

                //// Determine offset of 'User' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsUser, false, ColumnType.Middle);

                //// Determine offset of 'ClientID' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsClientId, false, ColumnType.Middle);

                //// Determine offset of 'Sess' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsSession, true, ColumnType.Middle);

                //// Determine offset of 'Uptime' column
                GenerateOffset(ref connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsUptime, true, ColumnType.Last);

                return null;
            }

            switch (GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsClientType))
            {
                case Constants.ClientTypeJava:
                    conn.ClientType = Constants.ClientTypeJavaLabel;
                    break;
                case Constants.ClientTypeC:
                    conn.ClientType = Constants.ClientTypeC;
                    break;
                case Constants.ClientTypeCSharp:
                    conn.ClientType = Constants.ClientTypeCSharpLabel;
                    break;
                default:
                    conn.ClientType = Constants.ClientTypeUnknownLabel;
                    break;
            }

            string flags = GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsFSXT);
            conn.SSLEnabled = (flags[1] == '+' || flags[1] == '/');
            conn.FaultTolerant = (flags[0] == '+');

            conn.ConnectionID = GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsId);
            conn.Host = GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsHost);
            conn.User = GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsUser);
            conn.SessionNumber = GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsSession);
            conn.SetUptime(GetColumnValue(connectionOffsetIndex, sb, Constants.OffsetHeaderConnectionsUptime));

            return conn;
        }

        private static TibcoConsumer ParseConsumerRow(StringBuilder sb, ref Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetIndex)
        {
            Dictionary<string, ColumnOffset> consumersOffsetIndex = offsetIndex[ParseMode.Consumers];
            TibcoConsumer consumer = new TibcoConsumer();

            // The console output for consumers produces two header rows. If
            // the parser is currently on the first row where the presence of
            // the token 'Msgs' exists, the parsing is skipped and returns
            // null. Afterwards, if no offsets have been configured, we assume
            // the current row contains the header labels. The columns offsets
            // are obtained here.
            if (consumersOffsetIndex.Count == 0)
            {
                //// Determine offset of 'Id' column
                GenerateOffset(ref consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersId, true, ColumnType.First);

                //// Determine offset of 'Conn' column
                GenerateOffset(ref consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersConn, true, ColumnType.Middle);

                //// Determine offset of 'User' column
                GenerateOffset(ref consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersUser, false, ColumnType.Middle);

                //// Determine offset of 'T' (consumer type) column
                GenerateOffset(ref consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersConsumerType, false, ColumnType.Middle);

                //// Determine offset of 'Queue' column
                GenerateOffset(ref consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersQueue, false, ColumnType.Middle);

                return null;
            }

            consumer.ConsumerID = GetColumnValue(consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersId);

            // Validate Consumer ID is an integer-based value.
            int consumerId;
            if (!int.TryParse(consumer.ConsumerID, out consumerId))
            {
                return null;
            }

            consumer.Active = (GetColumnValue(consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersConsumerType) == Constants.ConsumerTypeActive);   
            consumer.ConnectionID = GetColumnValue(consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersConn);
            consumer.User = GetColumnValue(consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersUser);
            consumer.Queue = GetColumnValue(consumersOffsetIndex, sb, Constants.OffsetHeaderConsumersQueue);

            return consumer;
        }

        private static void GenerateOffset(ref Dictionary<string, ColumnOffset> offsetIndex, StringBuilder sb, string headerColumnName, bool columnIsRightJustified, ColumnType type)
        {
            GenerateOffset(ref offsetIndex, sb, headerColumnName, columnIsRightJustified, type, 0);
        }

        private static void GenerateOffset(ref Dictionary<string, ColumnOffset> offsetIndex, StringBuilder sb, string headerColumnName, bool columnIsRightJustified, ColumnType type, int customOffset)
        {
            int columnLength = -1;
            int position = -1;

            switch (type)
            {
                case ColumnType.First:
                    // Obtain column length and offset
                    if (columnIsRightJustified)
                    {
                        // Column is right justified and is the first column
                        // Obtain current offset and label length to determine
                        // total length
                        int currentPos = sb.ToString().IndexOf(headerColumnName);
                        if (currentPos > -1)
                        {
                            position = 0 + customOffset;
                            columnLength = (currentPos + headerColumnName.Length) - position;
                        }
                        else
                        {
                            position = -1;
                        }
                    }
                    else
                    {
                        // Column is left justified and is the first column
                        columnLength = GetLengthToRight(sb, headerColumnName, out position, customOffset);
                    }
                    break;
                case ColumnType.Middle:
                    if (columnIsRightJustified)
                    {
                        // Column is right justified
                        columnLength = GetLengthToLeft(sb, headerColumnName, out position, customOffset);
                    }
                    else
                    {
                        // Column is left justified
                        columnLength = GetLengthToRight(sb, headerColumnName, out position, customOffset);
                    }
                    break;
                case ColumnType.Last:
                    if (columnIsRightJustified)
                    {
                        columnLength = GetLengthToLeft(sb, headerColumnName, out position, customOffset);
                    }
                    else
                    {
                        columnLength = GetLengthToRight(sb, headerColumnName, out position, customOffset);
                    }
                    break;
            }

            // Create new offset object and index
            ColumnOffset offset = new ColumnOffset(
                    headerColumnName,
                    position,
                    columnLength,
                    columnIsRightJustified);

            if(offset.Position > -1)
            {
                offsetIndex.Add(headerColumnName, offset);
            }
        }

        private static int GetLengthToRight(StringBuilder line, string headerColumnName, out int columnPosition)
        {
            return GetLengthToRight(line, headerColumnName, out columnPosition, 0);
        }

        private static int GetLengthToRight(StringBuilder line, string headerColumnName, out int columnPosition, int customOffset)
        {
            int columnLength = 0;

            // Obtain offset
            columnPosition = line.ToString().IndexOf(headerColumnName);

            // Obtain length (if the token exists)
            if (columnPosition > -1)
            {
                StringBuilder regExBuilder = new StringBuilder();
                regExBuilder.Append(headerColumnName);
                regExBuilder.Append(Constants.RegExSearchRight);

                // 2 is subtracted from the following matched reg ex result
                // to account for the extraneous character and the space
                // separator between the two columns.
                columnLength = (Regex.Match(line.ToString(), regExBuilder.ToString()).ToString().Length) - 2;
            }

            return columnLength;
        }

        private static int GetLengthToLeft(StringBuilder line, string headerColumnName, out int columnPosition)
        {
            return GetLengthToLeft(line, headerColumnName, out columnPosition, 0);
        }

        private static int GetLengthToLeft(StringBuilder line, string headerColumnName, out int columnPosition, int customOffset)
        {
            int columnLength = 0;
            columnPosition = -1;

            // Locate the column name in the line to be parsed
            int currentPos = line.ToString().IndexOf(headerColumnName);

            // If the token exists, perform calculations for length and
            // column position
            if (currentPos > -1)
            {
                // Obtain length
                StringBuilder regExBuilder = new StringBuilder();
                regExBuilder.Append(Constants.RegExSearchLeft);
                regExBuilder.Append(headerColumnName);

                // 2 is subtracted from the following matched reg ex result
                // to account for the extraneous character and the space
                // separator between the two columns.
                columnLength = (Regex.Match(line.ToString(), regExBuilder.ToString()).ToString().Length) - 2 - customOffset;

                // Calculate position
                int endOfColumnIdx = currentPos + headerColumnName.Length;
                columnPosition = endOfColumnIdx - columnLength;
            }

            return columnLength;
        }

        private static int GetOffset(Dictionary<ParseMode, Dictionary<string, ColumnOffset>> offsetIndex, ParseMode currentMode, string headerColumnName)
        {
            return offsetIndex[currentMode][headerColumnName].Position;
        }

        private static string GetColumnValue(Dictionary<string, ColumnOffset> offsetIndex, StringBuilder sb, string headerColumnName)
        {
            return GetColumnValue(offsetIndex, sb, headerColumnName, false);
        }

        private static string GetColumnValue(Dictionary<string, ColumnOffset> offsetIndex, StringBuilder sb, string headerColumnName, bool rawOutput)
        {
            if (rawOutput)
            {
                return sb.ToString(offsetIndex[headerColumnName].Position, offsetIndex[headerColumnName].ColumnLength);
            }
            else
            {
                return sb.ToString(offsetIndex[headerColumnName].Position, offsetIndex[headerColumnName].ColumnLength).Trim();
            }
        }

        #endregion //Private Methods

     }
}